package com.cx.repository;

import com.cx.config.LettuceRedisAdapter;
import com.cx.entity.RedisEntity;
import com.cx.property.LettuceProperties;
import com.cx.support.AsyncCallback;
import com.cx.utils.BeanHelper;
import com.cx.utils.JsonHelper;
import com.cx.utils.SerializeAdapter;
import com.google.common.collect.Lists;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.AbstractJPAQuery;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.async.RedisKeyAsyncCommands;
import io.lettuce.core.api.async.RedisStringAsyncCommands;
import io.lettuce.core.api.reactive.RedisReactiveCommands;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.*;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QSort;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.data.repository.support.PageableExecutionUtils.TotalSupplier;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

/**
 * QueryDsl specific extension of {@link SimpleJpaRepository} which adds implementation for
 * {@link QueryDslPredicateExecutor}.
 * 
 * @author Oliver Gierke
 * @author Thomas Darimont
 * @author Mark Paluch
 * @author Jocelyn Ntakpe
 * @author Christoph Strobl
 */
public class BaseQueryDslJpaRepository<T extends RedisEntity<ID>, ID extends Serializable> extends QueryDslJpaRepository<T, ID> {

	private static final EntityPathResolver DEFAULT_ENTITY_PATH_RESOLVER = SimpleEntityPathResolver.INSTANCE;

	private final EntityPath<T> path;
	private final PathBuilder<T> builder;
	private final Querydsl querydsl;

    private final EntityManager em;

    private final Class<T> clazz;

    private final String clazzName;

    private final LettuceRedisAdapter lettuceRedisAdapter;

    private final LettuceProperties lettuceProperties;

	/**
	 * Creates a new {@link BaseQueryDslJpaRepository} from the given domain class and {@link EntityManager} and uses the
	 * given {@link EntityPathResolver} to translate the domain class into an {@link EntityPath}.
	 * 
	 * @param entityInformation must not be {@literal null}.
	 * @param em must not be {@literal null}.
	 */
	public BaseQueryDslJpaRepository(JpaEntityInformation<T, ID> entityInformation, Class<T> domainClass, EntityManager em, LettuceRedisAdapter lra) {
		super(entityInformation, em);
		this.path = DEFAULT_ENTITY_PATH_RESOLVER.createPath(entityInformation.getJavaType());
		this.builder = new PathBuilder<T>(path.getType(), path.getMetadata());
		this.querydsl = new Querydsl(em, builder);

        this.clazz = domainClass;
        this.clazzName = domainClass.getSimpleName().toLowerCase();
        this.em = em;
        this.lettuceRedisAdapter = lra;
        this.lettuceProperties = SerializeAdapter.lettuceProperties();
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findOne(com.mysema.query.types.Predicate)
	 */
	@Override
	public T findOne(final Predicate predicate) {
        final int conditionsHashcode = SerializeAdapter.serialize(predicate).hashCode();
        final String idskey = BeanHelper.key(keyspace(), "findOne", new String[]{"predicate"}, new Object[]{conditionsHashcode});
        try {
            final String entitykey = entityKey(idskey);
            if(StringUtils.isNotBlank(entitykey)) {
                final T entity = getOnlyOne(entitykey);

                if (!ObjectUtils.isEmpty(entity)) {
                    return entity;
                }
            }

            final T t = createQuery(predicate).select(path).fetchOne();

            if(null == t) {
                return t;
            }

            lettuceRedisAdapter.batchProcessAsync(new AsyncCallback() {
                @Override
                public void asyncInvoke(RedisAsyncCommands<String, Object> commands) {
//                    commands.hmset(key(t.getId()), BeanHelper.toHash(t));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(t.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(t));
                    stringAsyncCommands.setex(idskey, lettuceProperties.getCacheLevel2Ttl(), String.valueOf(t.getId()));
                }

                @Override
                public void asyncInvoke(RedisReactiveCommands<String, Object> commands) {
//                    commands.hmset(key(t.getId()), BeanHelper.toHash(t));
                    final RedisStringAsyncCommands<String, Object> stringAsyncCommands = (RedisStringAsyncCommands<String, Object>)commands;
                    stringAsyncCommands.setex(key(t.getId()), lettuceProperties.getCacheLevel2Ttl(), JsonHelper.serialize(t));
                    stringAsyncCommands.setex(idskey, lettuceProperties.getCacheLevel2Ttl(), String.valueOf(t.getId()));
                }
            });

            return t;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate)
	 */
	@Override
	public List<T> findAll(final Predicate predicate) {
        return findAllByCache(predicate, null, null);
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate, com.mysema.query.types.OrderSpecifier<?>[])
	 */
	@Override
	public List<T> findAll(final Predicate predicate, final OrderSpecifier<?>... orders) {
        return findAllByCache(predicate, null, orders);
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate, org.springframework.data.domain.Sort)
	 */
	@Override
	public List<T> findAll(final Predicate predicate, final Sort sort) {
        return findAllByCache(predicate, sort, null);
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.OrderSpecifier[])
	 */
	@Override
	public List<T> findAll(final OrderSpecifier<?>... orders) {
        return findAllByCache(null, null, orders);
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#findAll(com.mysema.query.types.Predicate, org.springframework.data.domain.Pageable)
	 */
	@Override
	public Page<T> findAll(final Predicate predicate, final Pageable pageable) {
        final String[] paramnames = new String[2];
        final Object[] paramvals = new Object[2];
        if(!ObjectUtils.isEmpty(predicate)){
            paramnames[0] = "predicate";
            paramvals[0] = SerializeAdapter.serialize(predicate).hashCode();
        }
        if(!ObjectUtils.isEmpty(pageable)){
            paramnames[1] = "pageable";
            paramvals[1] = SerializeAdapter.serialize(pageable).hashCode();
        }

        final String idskey = BeanHelper.key(keyspace(), "findAll", paramnames, paramvals);
        final String countkey = idskey+":count";
        try {
            final List<String> entitykeys = entityKeys(idskey);
            if(!CollectionUtils.isEmpty(entitykeys)) {
//                final List<Map<String, Object>> result = lettuceRedisAdapter.batchHgetall(entitykeys);
                final List<String> result = lettuceRedisAdapter.batchGet(entitykeys, String.class);
                final List<T> finalEntities = Lists.newArrayListWithCapacity(result.size());
//                result.stream().filter(p->null!=p).forEach(map -> {
//
//                    final T entity = BeanHelper.fromHash(map, clazz);
//                    if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
//                        finalEntities.add(entity);
//                    }
//                });
                result.stream().filter(p->null!=p).forEach(t -> {

                    final T entity = JsonHelper.deserialize(t, clazz);
                    if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
                        finalEntities.add(entity);
                    }
                });

                if (!CollectionUtils.isEmpty(finalEntities) && !CollectionUtils.isEmpty(entitykeys)) {
                    final long countval = Long.valueOf(lettuceRedisAdapter.get(countkey, String.class)).longValue();
                    return new PageImpl(finalEntities, pageable, countval);
                }
            }

            final Page<T> result = findAllJpa(predicate, pageable);

            if(CollectionUtils.isEmpty(result.getContent())){
                return result;
            }

            final List<String> ids = Lists.newArrayListWithCapacity(10);
//            final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//            result.getContent().iterator().forEachRemaining(entity -> {
//                ids.add(String.valueOf(entity.getId()));
//
//                dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//            });
            final Map<String, Object> dataMap = new HashMap(10);
            result.getContent().iterator().forEachRemaining(entity -> {
                ids.add(String.valueOf(entity.getId()));

                dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
            });

//            lettuceRedisAdapter.batchHmset(dataMap);
            lettuceRedisAdapter.batchSetex(dataMap);
            lettuceRedisAdapter.setexlist(idskey, ids);
            lettuceRedisAdapter.setex(countkey, String.valueOf(result.getTotalElements()));

            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

    private Page<T> findAllJpa(final Predicate predicate, final Pageable pageable) {

        final JPQLQuery<?> countQuery = createCountQuery(predicate);
        final JPQLQuery<T> query = querydsl.applyPagination(pageable, createQuery(predicate).select(path));

        return PageableExecutionUtils.getPage(query.fetch(), pageable, new TotalSupplier() {

            @Override
            public long get() {
                return countQuery.fetchCount();
            }
        });
    }

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#count(com.mysema.query.types.Predicate)
	 */
	@Override
	public long count(final Predicate predicate) {
        final int conditionsHashcode = SerializeAdapter.serialize(predicate).hashCode();
        final String idskey = BeanHelper.key(keyspace(), "count", new String[]{"count"}, new Object[]{conditionsHashcode});
        try {
            final String countkey = countKey(idskey);
            if(StringUtils.isNotBlank(countkey)){
                final Long count = Long.valueOf(countkey);
                if(null != count) {
                    return count;
                }
            }

            final long l = createQuery(predicate).fetchCount();

            lettuceRedisAdapter.setex(idskey, String.valueOf(l));

            return l;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.querydsl.QueryDslPredicateExecutor#exists(com.mysema.query.types.Predicate)
	 */
	@Override
	public boolean exists(final Predicate predicate) {
		return this.count(predicate) > 0;
	}

	/**
	 * Creates a new {@link JPQLQuery} for the given {@link Predicate}.
	 * 
	 * @param predicate
	 * @return the Querydsl {@link JPQLQuery}.
	 */
	protected JPQLQuery<?> createQuery(final Predicate... predicate) {
        AbstractJPAQuery<?, ?> query = querydsl.createQuery(path).where(predicate);
        final CrudMethodMetadata metadata = getRepositoryMethodMetadata();

        if (metadata == null) {
            return query;
        }

        final LockModeType type = metadata.getLockModeType();
        query = type == null ? query : query.setLockMode(type);

        for (Entry<String, Object> hint : getQueryHints().entrySet()) {
            query.setHint(hint.getKey(), hint.getValue());
        }

        return query;
	}

	/**
	 * Creates a new {@link JPQLQuery} count query for the given {@link Predicate}.
	 *
	 * @param predicate, can be {@literal null}.
	 * @return the Querydsl count {@link JPQLQuery}.
	 */
	protected JPQLQuery<?> createCountQuery(final Predicate predicate) {
        final AbstractJPAQuery<?, ?> query = querydsl.createQuery(path).where(predicate);

        final CrudMethodMetadata metadata = getRepositoryMethodMetadata();

        if (metadata == null) {
            return query;
        }

        for (Entry<String, Object> hint : metadata.getQueryHints().entrySet()) {
            query.setHint(hint.getKey(), hint.getValue());
        }

        return query;
	}

	/**
	 * Executes the given {@link JPQLQuery} after applying the given {@link OrderSpecifier}s.
	 * 
	 * @param query must not be {@literal null}.
	 * @param orders must not be {@literal null}.
	 * @return
	 */
	private List<T> executeSorted(final JPQLQuery<T> query, final OrderSpecifier<?>... orders) {
		return executeSorted(query, new QSort(orders));
	}

	/**
	 * Executes the given {@link JPQLQuery} after applying the given {@link Sort}.
	 * 
	 * @param query must not be {@literal null}.
	 * @param sort must not be {@literal null}.
	 * @return
	 */
	private List<T> executeSorted(final JPQLQuery<T> query, final Sort sort) {
		return querydsl.applySorting(sort, query).fetch();
	}

    private String keyspace(){
        return lettuceProperties.cacheLevel2Pre() + clazzName;
    }

    private String key(final ID id){
        return keyspace() + ":ids:" + id;
    }

    private String countKey(final String key){
        final Boolean hasKey = lettuceRedisAdapter.exists(key);
        if(BooleanUtils.isFalse(hasKey)){
            return null;
        }

        return lettuceRedisAdapter.get(key, String.class);
    }

    private String entityKey(final String key){
        final Boolean hasKey = lettuceRedisAdapter.exists(key);
        if(BooleanUtils.isFalse(hasKey)){
            return null;
        }

        return lettuceRedisAdapter.get(key, String.class);
    }

	private List<String> entityKeys(final String key){
        final Boolean hasKey = lettuceRedisAdapter.exists(key);
		if(BooleanUtils.isFalse(hasKey)){
			return null;
		}

		return lettuceRedisAdapter.getlist(key, String.class);
	}

    private T getOnlyOne(final String key){
        final Boolean hasKey = lettuceRedisAdapter.exists(key);
        if(BooleanUtils.isFalse(hasKey)){
            return null;
        }

//        final Map<String, Object> map = lettuceRedisAdapter.hgetall(key);
//        return BeanHelper.fromHash(map, clazz);
        final String entity = lettuceRedisAdapter.get(key, String.class);
        return JsonHelper.deserialize(entity, clazz);
    }

    private List<T> findAllByCache(final Predicate predicate, final Sort sort, final OrderSpecifier<?>... orders) {
        final String[] paramnames = new String[3];
        final Object[] paramvals = new Object[3];
        if(!ObjectUtils.isEmpty(predicate)){
            paramnames[0] = "predicate";
            paramvals[0] = SerializeAdapter.serialize(predicate).hashCode();
        }
        if(!ObjectUtils.isEmpty(sort)){
            paramnames[1] = "sort";
            paramvals[1] = SerializeAdapter.serialize(sort).hashCode();
        }
        if(!ObjectUtils.isEmpty(orders)){
            paramnames[2] = "orders";
            paramvals[2] = SerializeAdapter.serialize(orders).hashCode();
        }

        final String idskey = BeanHelper.key(keyspace(), "findAll", paramnames, paramvals);
        try {
            final List<String> entitykeys = entityKeys(idskey);
            if(!CollectionUtils.isEmpty(entitykeys)) {
//                final List<Map<String, Object>> result = lettuceRedisAdapter.batchHgetall(entitykeys);
                final List<String> result = lettuceRedisAdapter.batchGet(entitykeys, String.class);
                final List<T> finalEntities = Lists.newArrayListWithCapacity(result.size());
//                result.stream().filter(p->null!=p).forEach(map -> {
//
//                    final T entity = BeanHelper.fromHash(map, clazz);
//                    if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
//                        finalEntities.add(entity);
//                    }
//                });
                result.stream().filter(p->null!=p).forEach(t -> {

                    final T entity = JsonHelper.deserialize(t, clazz);
                    if (Objects.nonNull(entity) && Objects.nonNull(entity.getId())) {
                        finalEntities.add(entity);
                    }
                });

                if (!CollectionUtils.isEmpty(finalEntities) && !CollectionUtils.isEmpty(entitykeys)) {
                    return finalEntities;
                }
            }

            List<T> result = null;
            if(!ObjectUtils.isEmpty(predicate) && ObjectUtils.isEmpty(sort) && ArrayUtils.isEmpty(orders)){
                result = createQuery(predicate).select(path).fetch();
            }
            if(!ObjectUtils.isEmpty(predicate) && ObjectUtils.isEmpty(sort) && ArrayUtils.isNotEmpty(orders)){
                result = executeSorted(createQuery(predicate).select(path), orders);
            }
            if(!ObjectUtils.isEmpty(predicate) && !ObjectUtils.isEmpty(sort) && ArrayUtils.isEmpty(orders)){
                result = executeSorted(createQuery(predicate).select(path), sort);
            }
            if(ObjectUtils.isEmpty(predicate) && ObjectUtils.isEmpty(sort) && ArrayUtils.isNotEmpty(orders)){
                result = executeSorted(createQuery(new Predicate[0]).select(path), orders);
            }
            if(CollectionUtils.isEmpty(result)){
                return result;
            }

            final List<String> ids = Lists.newArrayListWithCapacity(10);
//            final Map<String, Map<String, Object>> dataMap = new HashMap(10);
//            result.iterator().forEachRemaining(entity -> {
//                ids.add(String.valueOf(entity.getId()));
//
//                dataMap.put(key(entity.getId()), BeanHelper.toHash(entity));
//            });
            final Map<String, Object> dataMap = new HashMap(10);
            result.iterator().forEachRemaining(entity -> {
                ids.add(String.valueOf(entity.getId()));

                dataMap.put(key(entity.getId()), JsonHelper.serialize(entity));
            });

//            lettuceRedisAdapter.batchHmset(dataMap);
            lettuceRedisAdapter.batchSetex(dataMap);

            lettuceRedisAdapter.setexlist(idskey, ids);

            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}